/**
 * 
 */
package fhq.hcmute.qrcode.encoder;

import static fhq.hcmute.qrcode.util.QRCodeException.APPEND_QR_INFORMATION_FAILD;
import static fhq.hcmute.qrcode.util.QRCodeException.ERROR_CORRECTION_CODEWORDS_FAILD;

import java.io.UnsupportedEncodingException;

import fhq.hcmute.qrcode.QRCode;
import fhq.hcmute.qrcode.common.DataBlock;
import fhq.hcmute.qrcode.common.DataMode;
import fhq.hcmute.qrcode.common.ErrorCorrectionLevel;
import fhq.hcmute.qrcode.common.MaskPattern;
import fhq.hcmute.qrcode.common.Version;
import fhq.hcmute.qrcode.reedsolomon.ReedSolomon;
import fhq.hcmute.qrcode.util.AlphanumericTable;
import fhq.hcmute.qrcode.util.QRCodeException;

/**
 * @author Hien Nguyen Le
 * @Email: nglehien@gmail.com
 */
public class Encoder {
	private BitArray bitArray;
	private DataMode dataMode;
	private Version version;
	private String contents;
	private ErrorCorrectionLevel errorCorrectionLevel;
	public static final int NOT_SELECTED_VERSION = 0;
	public static final String DEFAULT_BYTE_MODE_ENCODING = "UTF-8";

	public Encoder() {
		bitArray = new BitArray();
	}

	/**
	 * @param dataMode
	 * @param version
	 * @param contents
	 */
	public Encoder(DataMode dataMode, ErrorCorrectionLevel level,
			String contents) {
		this(dataMode, level, contents, NOT_SELECTED_VERSION);
		this.version = pickVersion(contents, dataMode);
	}

	/**
	 * Get a QR code with specific Version
	 * 
	 * @param dataMode
	 * @param level
	 * @param contents
	 * @param versionNumber
	 */
	public Encoder(DataMode dataMode, ErrorCorrectionLevel level,
			String contents, int versionNumber) {
		this.dataMode = dataMode;
		this.contents = contents;
		this.bitArray = new BitArray();
		this.errorCorrectionLevel = level;
		if (versionNumber != NOT_SELECTED_VERSION)
			this.version = Version.getVersionByNumber(versionNumber);
	}

	public void appendHeader() {
		bitArray.append(this.dataMode, this.version, this.contents);
	}

	public void appendContents() {
		if (dataMode.equals(DataMode.NUMERIC)) {
			String[] temp = contents.split("(?<=\\G.{3})");
			for (String i : temp) {
				int value = Integer.parseInt(i);
				if (i.length() % 3 == 0) {
					bitArray.append(10, value);
				} else if (i.length() % 3 == 1) {
					bitArray.append(4, value);
				} else if (i.length() % 3 == 2) {
					bitArray.append(7, value);
				}
			}
		} else if (dataMode.equals(DataMode.ALPHANUMERIC)) {
			String[] temp = contents.split("(?<=\\G.{2})");
			for (String i : temp) {
				int value1 = AlphanumericTable.getValueByChar(i.charAt(0));
				if (i.length() % 2 == 0) {
					value1 *= 45;
					int value2 = AlphanumericTable.getValueByChar(i.charAt(1));
					bitArray.append(11, value1 + value2);
				} else if (i.length() % 2 == 1) {
					bitArray.append(6, value1);
				}
			}
		} else {
			try {
				byte[] temp = contents.getBytes(DEFAULT_BYTE_MODE_ENCODING);
				for (byte i : temp) {
					bitArray.append(8, i);
				}
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	/**
	 * Make sure the correct length.( depends on how many data bits we are
	 * required to generate for the version and error correction the we are
	 * using. We add four 0s to the end if our string length is not equal with
	 * total bits. THE FINAL RESULT IS NOT OVER TOTAL BITS LENGTH.
	 */
	public void terminateBits() {
		int totalBitDataCodewords = version
				.getTotalDataCodewords(errorCorrectionLevel) * 8;
		int terminateBits = totalBitDataCodewords - this.bitArray.getSize();
		if (terminateBits <= 4) {
			bitArray.append(terminateBits, 0);
		} else {
			bitArray.append(4, 0);
		}
		delimit8Bits();
	}

	/**
	 * Delimit the string into 8-bit words, then pad with 0s if necessary If the
	 * last group is not 8-bits long, we pad it on right with 0s.
	 */
	private void delimit8Bits() {
		if (bitArray.getSize() % 8 != 0) {
			int pad = 8 - bitArray.getSize() % 8;
			bitArray.append(pad, 0);
		}
	}

	/**
	 * if our bit string is not long enough yet, there are two special bit
	 * strings, 11101100 and 00010001, the QR code specification requires us to
	 * put at the end of our string.
	 */
	public void appendSpecialBitStrings() {
		int totalBitDataCodewords = version
				.getTotalDataCodewords(errorCorrectionLevel) * 8;
		int pad = totalBitDataCodewords - this.bitArray.getSize();
		pad /= 8;
		for (int i = 0; i < pad; i++) {
			if (i % 2 == 0) {
				bitArray.append("11101100");
			} else {
				bitArray.append("00010001");
			}
		}
	}

	/**
	 * QR code include error correction code words. These blocks of redundant
	 * data ensure the QR code can still be read even if a portion of it is
	 * unreadable
	 * 
	 * @throws QRCodeException
	 */
	public void createErrorCorrectionCodeWords() throws QRCodeException {
		Version.ECBlocks blocks = version
				.getCurrentECBlock(errorCorrectionLevel);
		Version.ECB[] ecb = blocks.getECBlocks();
		DataBlock dataBlock = new DataBlock();
		ReedSolomon reedSolomon = new ReedSolomon();
		int currentIndex = 0;
		for (int i = 0; i < ecb.length; i++) {
			for (int j = 0; j < ecb[i].getECBlockCount(); j++) {
				int numOfDataCodewords = ecb[i].getNumberOfDataCodewords();
				int errorCorrectionCodewords = blocks.getECCodewordsPerBlock();
				int[] dataCodeWords = this.bitArray.getIntegerArray(
						currentIndex, numOfDataCodewords + currentIndex - 1);
				int[] errorCCodeWords;
				try {
					errorCCodeWords = reedSolomon.encode(dataCodeWords,
							errorCorrectionCodewords);
				} catch (QRCodeException e) {
					throw new QRCodeException(ERROR_CORRECTION_CODEWORDS_FAILD);
				}
				dataBlock.add(dataCodeWords, errorCCodeWords);
				currentIndex += numOfDataCodewords;
			}
		}
		this.bitArray.clear();

		/*
		 * Assemble the final sequence by taking data codewords and error
		 * correction codewords from each block in turn.
		 * 
		 * @SEE MORE: ISO/IEC 18004, Figure 15 - Constructing the final message
		 * codeword sequence. Page.46
		 */
		for (int i = 0; i < dataBlock.getMaxDataCodeWord(); i++) {
			for (DataBlock.Block b : dataBlock.getBlocks()) {
				if (i >= b.getDataCodeWords().length) {
					continue;
				}
				int value = b.getDataCodeWords()[i];
				bitArray.append(8, value);
			}
		}

		for (int i = 0; i < dataBlock.getMaxErrorCCodeWord(); i++) {
			for (DataBlock.Block b : dataBlock.getBlocks()) {
				if (i >= b.getErrorCCodewords().length) {
					continue;
				}
				int value = b.getErrorCCodewords()[i];
				bitArray.append(8, value);
			}
		}
		System.out.println();
	}

	/**
	 * auto pick the best of version base on DataMode, ErrorCorrectionLevel
	 * 
	 * @param contents
	 * @param mode
	 * @param level
	 * @return
	 */
	public Version pickVersion(String contents, DataMode mode) {
		int inputBits = mode.getTotalBitRespective(contents) + 4;
		for (int i = 1; i < 40; i++) {
			Version version = Version.getVersionByNumber(i);
			int totalInputBits = inputBits + mode.getCharacterCount(version);
			int maxDataBits = (version
					.getTotalDataCodewords(errorCorrectionLevel)) * 8;
			if (maxDataBits >= totalInputBits) {
				return version;
			}
		}
		return Version.getVersionByNumber(40);
	}

	public QRCode chooseBestQRCode() throws QRCodeException {
		QRCode bestQRCode = null;
		int penaltyScore = Integer.MAX_VALUE;
		for (int i = 0; i < 8; i++) {
			QRCode qrCode = new QRCode(errorCorrectionLevel,
					MaskPattern.parse(i), version, dataMode, contents);
			try {
				qrCode.appendVersionInfomation();
				qrCode.appendFormatInformation();
			} catch (QRCodeException e) {
				throw new QRCodeException(APPEND_QR_INFORMATION_FAILD);
			}
			qrCode.appendBitDatas(bitArray);
			int score = (new PenaltyScore(qrCode)).calculate();
			if (score < penaltyScore) {
				penaltyScore = score;
				bestQRCode = qrCode;
			}
		}
		return bestQRCode;

	}

	public DataMode getDataMode() {
		return dataMode;
	}

	public void setDataMode(DataMode dataMode) {
		this.dataMode = dataMode;
	}

	public Version getVersion() {
		return version;
	}

	public void setVersion(Version version) {
		this.version = version;
	}

	public String getContents() {
		return contents;
	}

	public void setContents(String contents) {
		this.contents = contents;
	}

	public void setBitArray(BitArray bitArray) {
		this.bitArray = bitArray;
	}

	public BitArray getBitArray() {
		return bitArray;
	}
}
